<div id="app" class="container">
  <div class="my-4">
    <h1>Applications</h1>
    <div>Applications are refreshed only when Client is restarted</div>
  </div>
  <div class="card p-4">
    <table class="table">
      <thead>
        <tr>
          <th scope="col">Name</th>
          <th scope="col">Actions</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(app,i) in apps" :key="i">
          <td>{{app.name}}</td>
          <td>
            <button class="btn btn-primary" @click="editApp(i)">
              <i class="fas fa-edit"></i> Edit
            </button>
            <button class="btn btn-danger" @click="showDeleteForm(i)">
              <i class="fas fa-trash"></i> Delete
            </button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
  <div class="edit-form card mt-2" v-if="showEditForm">
    <div class="p-4">
      <!--name-->
      <div class="mb-3">
        <label for="appName" class="form-label">Application Name</label>
        <input
          type="text"
          class="form-control"
          id="appName"
          aria-describedby="appNameHelp"
          v-model="editForm.name"
        />
        <div id="appNameHelp" class="form-text">
          Application Name, as shown on Moonlight
        </div>
      </div>
      <!--output-->
      <div class="mb-3">
        <label for="appOutput" class="form-label">Output</label>
        <input
          type="text"
          class="form-control monospace"
          id="appOutput"
          aria-describedby="appOutputHelp"
          v-model="editForm.output"
        />
        <div id="appOutputHelp" class="form-text">
          The file where the output of the command is stored, if it is not
          specified, the output is ignored
        </div>
      </div>
      <!--prep-cmd-->
      <div class="mb-3">
        <label for="excludeGlobalPrep" class="form-label"
          >Global Prep Commands</label
        >
        <select
          id="excludeGlobalPrep"
          class="form-select"
          v-model="editForm['exclude-global-prep-cmd']"
        >
          <option v-for="val in [false, true]" :value="val">
            {{ !val ? 'Enabled' : 'Disabled' }}
          </option>
        </select>
        <div class="form-text">
          Enable/Disable the execution of Global Prep Commands for this
          application.
        </div>
      </div>
      <div class="mb-3">
        <label for="appName" class="form-label">Command Preparations</label>
        <div class="form-text">
          A list of commands to be run before/after this application.<br />
          If any of the prep-commands fail, starting the application is aborted.
        </div>
        <div
          class="d-flex justify-content-start mb-3 mt-3"
          v-if="editForm['prep-cmd'].length === 0"
        >
          <button class="btn btn-success" @click="addPrepCmd">
            <i class="fas fa-plus mr-1"></i> Add Commands
          </button>
        </div>
        <table class="table" v-if="editForm['prep-cmd'].length > 0">
          <thead>
            <tr>
              <th scope="col"><i class="fas fa-play"></i> Do Command</th>
              <th scope="col"><i class="fas fa-undo"></i> Undo Command</th>
              <th scope="col" v-if="platform === 'windows'">
                <i class="fas fa-shield-alt"></i> Run as Admin
              </th>
              <th scope="col"></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(c, i) in editForm['prep-cmd']">
              <td>
                <input
                  type="text"
                  class="form-control monospace"
                  v-model="c.do"
                />
              </td>
              <td>
                <input
                  type="text"
                  class="form-control monospace"
                  v-model="c.undo"
                />
              </td>
              <td v-if="platform === 'windows'">
                <div class="form-check">
                  <input
                    type="checkbox"
                    class="form-check-input"
                    :id="'prep-cmd-admin-' + i"
                    v-model="c.elevated"
                    true-value="true"
                    false-value="false"
                  />
                  <label :for="'prep-cmd-admin-' + i" class="form-check-label"
                    >Elevated</label
                  >
                </div>
              </td>
              <td>
                <button
                  class="btn btn-danger"
                  @click="$delete(editForm['prep-cmd'], i)"
                >
                  <i class="fas fa-trash"></i>
                </button>
                <button class="btn btn-success" @click="addPrepCmd">
                  <i class="fas fa-plus"></i>
                </button>
              </td>
            </tr>
          </tbody>
        </table>
      </div>
      <!--detatched-->
      <div class="mb-3">
        <label for="appName" class="form-label">Detached Commands</label>
        <div
          v-for="(c,i) in editForm.detached"
          class="d-flex justify-content-between my-2"
        >
          <pre>{{c}}</pre>
          <button
            class="btn btn-danger mx-2"
            @click="editForm.detached.splice(i,1)"
          >
            &times;
          </button>
        </div>
        <div class="d-flex justify-content-between">
          <input
            type="text"
            class="form-control monospace"
            v-model="detachedCmd"
          />
          <button
            class="btn btn-success mx-2"
            @click="editForm.detached.push(detachedCmd);detachedCmd = '';"
          >
            +
          </button>
        </div>
        <div class="form-text">
          A list of commands to be run and forgotten about
        </div>
      </div>
      <!--command-->
      <div class="mb-3">
        <label for="appCmd" class="form-label">Command</label>
        <input
          type="text"
          class="form-control monospace"
          id="appCmd"
          aria-describedby="appCmdHelp"
          v-model="editForm.cmd"
        />
        <div id="appCmdHelp" class="form-text">
          The main application, if it is not specified, a process is started
          that sleeps indefinitely
        </div>
      </div>
      <!--working dir-->
      <div class="mb-3">
        <label for="appWorkingDir" class="form-label">Working Directory</label>
        <input
          type="text"
          class="form-control monospace"
          id="appWorkingDir"
          aria-describedby="appWorkingDirHelp"
          v-model="editForm['working-dir']"
        />
        <div id="appWorkingDirHelp" class="form-text">
          The working directory that should be passed to the process. For
          example, some applications use the working directory to search for
          configuration files. If not set, Sunshine will default to the parent
          directory of the command
        </div>
      </div>
      <!-- elevation -->
      <div class="mb-3 form-check" v-if="platform === 'windows'">
        <label for="appElevation" class="form-check-label"
          >Run as administrator</label
        >
        <input
          type="checkbox"
          class="form-check-input"
          id="appElevation"
          v-model="editForm.elevated"
          true-value="true"
          false-value="false"
        />
        <div class="form-text">
          This can be necessary for some applications that require administrator
          permissions to run properly.
        </div>
      </div>
      <!-- auto-detach -->
      <div class="mb-3 form-check">
        <label for="autoDetach" class="form-check-label"
          >Continue streaming if the application exits quickly</label
        >
        <input
          type="checkbox"
          class="form-check-input"
          id="autoDetach"
          v-model="editForm['auto-detach']"
          true-value="true"
          false-value="false"
        />
        <div class="form-text">
          This will attempt to automatically detect launcher-type apps that close
          quickly after launching another program or instance of themselves. When
          a launcher-type app is detected, it is treated as a detached app.
        </div>
      </div>
      <!-- Image path -->
      <div class="mb-3">
        <label for="appImagePath" class="form-label">Image</label>
        <div class="input-group dropup">
          <input
            type="text"
            class="form-control monospace"
            id="appImagePath"
            aria-describedby="appImagePathHelp"
            v-model="editForm['image-path']"
          />
          <button
            class="btn btn-secondary dropdown-toggle"
            type="button"
            id="findCoverToggle"
            data-bs-toggle="dropdown"
            data-bs-auto-close="outside"
            aria-expanded="false"
            v-dropdown-show="showCoverFinder"
            ref="coverFinderDropdown"
          >
            Find Cover
          </button>
          <div
            class="dropdown-menu dropdown-menu-end w-50 cover-finder overflow-hidden"
            aria-labelledby="findCoverToggle"
          >
            <div class="modal-header">
              <h4 class="modal-title">Covers Found</h4>
              <button
                type="button"
                class="btn-close"
                aria-label="Close"
                @click="closeCoverFinder"
              ></button>
            </div>
            <div
              class="modal-body cover-results px-3 pt-3"
              :class="{ busy: coverFinderBusy }"
            >
              <div class="row">
                <div
                  v-if="coverSearching"
                  class="col-12 col-sm-6 col-lg-4 mb-3"
                >
                  <div class="cover-container">
                    <div class="spinner-border" role="status">
                      <span class="visually-hidden">Loading...</span>
                    </div>
                  </div>
                </div>
                <div
                  v-for="(cover,i) in coverCandidates"
                  :key="'i'"
                  class="col-12 col-sm-6 col-lg-4 mb-3"
                  @click="useCover(cover)"
                >
                  <div class="cover-container result">
                    <img class="rounded" :src="cover.url" />
                  </div>
                  <label class="d-block text-nowrap text-center text-truncate">
                    {{cover.name}}
                  </label>
                </div>
              </div>
            </div>
          </div>
        </div>
        <div id="appImagePathHelp" class="form-text">
          Application icon/picture/image path that will be sent to client. Image
          must be a PNG file. If not set, Sunshine will send default box image.
        </div>
      </div>
      <div class="env-hint">
        <div class="form-text"><b>About Environment Variables: </b> All commands get these environment variables by default: </div>
        <table>
          <tr><td><b>Var Name</b></td><td><b></b></td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_APP_ID</td><td>App ID</td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_APP_NAME</td><td>App Name</td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_CLIENT_WIDTH</td><td>The Width requested by the client</td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_CLIENT_HEIGHT</td><td>The Height requested by the client</td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_CLIENT_FPS</td><td>The FPS requested by the client</td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_CLIENT_HDR</td><td>(true/false) if HDR is enabled by the client</td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_CLIENT_GCMAP</td><td>(int) the requested gamepad mask, in a bitset/bitfield format</td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_CLIENT_HOST_AUDIO</td><td>(true/false) if the client has requested host audio</td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_CLIENT_ENABLE_SOPS</td><td>(true/false) if the client has requested the option to optimize the game for optimal streaming</td></tr>
          <tr><td style="font-family: monospace">SUNSHINE_CLIENT_AUDIO_CONFIGURATION</td><td>The Audio Configuration requested by the client (2.0/5.1/7.1)</td></tr>
        </table>
        <div class="form-text" v-if="platform === 'windows'"><b>Example - QRes for Resolution Automation:</b> <pre>cmd /C &lt;qres path&gt;\QRes.exe /X:%SUNSHINE_CLIENT_WIDTH% /Y:%SUNSHINE_CLIENT_HEIGHT%</pre></div>
        <div class="form-text" v-else-if="platform === 'linux'"><b>Example - Xrandr for Resolution Automation:</b> <pre>sh -c "xrandr --output HDMI-1 --mode \"${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT}\" --rate 60"</pre></div>
        <div class="form-text" v-else-if="platform === 'macos'"><b>Example - displayplacer for Resolution Automation:</b> <pre>sh -c "displayplacer "id:<screenId> res:${SUNSHINE_CLIENT_WIDTH}x${SUNSHINE_CLIENT_HEIGHT} hz:60 scaling:on origin:(0,0) degree:0""</pre></div>
        <div class="form-text"><a href="https://docs.lizardbyte.dev/projects/sunshine/en/latest/about/guides/app_examples.html" target="_blank">See More</a></div>
      </div>
      <!--buttons-->
      <div class="d-flex">
        <button @click="showEditForm = false" class="btn btn-secondary m-2">
          Cancel
        </button>
        <button class="btn btn-primary m-2" @click="save">Save</button>
      </div>
    </div>
  </div>
  <div class="mt-2" v-else>
    <button class="btn btn-primary" @click="newApp">
      <i class="fas fa-plus"></i> Add New
    </button>
  </div>
</div>
<script>
  Vue.directive('dropdown-show', {
    bind: function (el, binding) {
      el.addEventListener('show.bs.dropdown', binding.value);
    }
  });

  new Vue({
    el: "#app",
    data() {
      return {
        apps: [],
        showEditForm: false,
        editForm: null,
        detachedCmd: "",
        coverSearching: false,
        coverFinderBusy: false,
        coverCandidates: [],
        platform: "",
      };
    },
    created() {
      fetch("/api/apps")
        .then((r) => r.json())
        .then((r) => {
          console.log(r);
          this.apps = r.apps;
        });
      
      fetch("/api/config")
        .then(r => r.json())
        .then(r => this.platform = r.platform);
    },
    methods: {
      newApp() {
        this.editForm = {
          name: "",
          output: "",
          cmd: [],
          index: -1,
          "exclude-global-prep-cmd": false,
          elevated: false,
          "auto-detach": true,
          "prep-cmd": [],
          detached: [],
          "image-path": ""
        };
        this.editForm.index = -1;
        this.showEditForm = true;
      },
      editApp(id) {
        this.editForm = JSON.parse(JSON.stringify(this.apps[id]));
        this.$set(this.editForm, "index", id);
        if (this.editForm["prep-cmd"] === undefined)
          this.$set(this.editForm, "prep-cmd", []);
        if (this.editForm["detached"] === undefined)
          this.$set(this.editForm, "detached", []);
        if (this.editForm["exclude-global-prep-cmd"] === undefined)
          this.$set(this.editForm, "exclude-global-prep-cmd", false);
        if(this.editForm["elevated"] === undefined && this.platform === 'windows'){
          this.$set(this.editForm, "elevated", false);
        }
        if(this.editForm["auto-detach"] === undefined){
          this.$set(this.editForm, "auto-detach", true);
        }
        this.showEditForm = true;
      },
      showDeleteForm(id) {
        let resp = confirm(
          "Are you sure to delete " + this.apps[id].name + "?"
        );
        if (resp) {
          fetch("/api/apps/" + id, { method: "DELETE" }).then((r) => {
            if (r.status == 200) document.location.reload();
          });
        }
      },
      addPrepCmd() {
        let template = {
          do: "",
          undo: ""
        };

        if(this.platform === 'windows'){
          template = {...template, elevated: false};
        }

        this.editForm["prep-cmd"].push(template);
      },
      showCoverFinder($event) {
        this.coverCandidates = [];
        this.coverSearching = true;

        function getSearchBucket(name) {
          let bucket = name.substring(0, Math.min(name.length, 2)).toLowerCase().replaceAll(/[^a-z\d]/g, '');
          if (!bucket) {
            return '@';
          }
          return bucket;
        }

        function searchCovers(name) {
          if (!name) {
            return Promise.resolve([]);
          }
          let searchName = name.replaceAll(/\s+/g, '.').toLowerCase();
          let bucket = getSearchBucket(name);
          return fetch("https://db.lizardbyte.dev/buckets/" + bucket + ".json").then(function (r) {
            if (!r.ok) throw new Error("Failed to search covers");
            return r.json();
          }).then(maps => Promise.all(Object.keys(maps).map(id => {
            let item = maps[id];
            if (item.name.replaceAll(/\s+/g, '.').toLowerCase().startsWith(searchName)) {
              return fetch("https://db.lizardbyte.dev/games/" + id + ".json").then(function (r) {
                return r.json();
              }).catch(() => null);
            }
            return null;
          }).filter(item => item)))
            .then(results => results
              .filter(item => item && item.cover && item.cover.url)
              .map(game => {
                const thumb = game.cover.url;
                const dotIndex = thumb.lastIndexOf('.');
                const slashIndex = thumb.lastIndexOf('/');
                if (dotIndex < 0 || slashIndex < 0) {
                  return null;
                }
                const hash = thumb.substring(slashIndex + 1, dotIndex);
                return {
                  name: game.name,
                  key: "igdb_" + game.id,
                  url: "https://images.igdb.com/igdb/image/upload/t_cover_big/" + hash + ".jpg",
                  saveUrl: "https://images.igdb.com/igdb/image/upload/t_cover_big_2x/" + hash + ".png",
                }
              }).filter(item => item));
        }

        searchCovers(this.editForm["name"].toString())
          .then(list => this.coverCandidates = list)
          .finally(() => this.coverSearching = false);
      },
      closeCoverFinder() {
        const ref = this.$refs.coverFinderDropdown;
        if (!ref) {
          return;
        }
        const dropdown = this.coverFinderDropdown = bootstrap.Dropdown.getInstance(ref);
        if (!dropdown) {
          return;
        }
        dropdown.hide();
      },
      useCover(cover) {
        this.coverFinderBusy = true;
        fetch("/api/covers/upload", {
          method: "POST",
          body: JSON.stringify({
            key: cover.key,
            url: cover.saveUrl,
          })
        }).then(r => {
          if (!r.ok) throw new Error("Failed to download covers");
          return r.json();
        }).then(body => this.$set(this.editForm, "image-path", body.path))
          .then(() => this.closeCoverFinder())
          .finally(() => this.coverFinderBusy = false);
      },
      save() {
        this.editForm["image-path"] = this.editForm["image-path"].toString().replace(/"/g, '');
        fetch("/api/apps", {
          method: "POST",
          body: JSON.stringify(this.editForm),
        }).then((r) => {
          if (r.status == 200) document.location.reload();
        });
      },
    },
  });
</script>

<style>
  .precmd-head {
    width: 200px;
  }

  .monospace {
    font-family: monospace;
  }

  .cover-finder {
  }

  .cover-finder .cover-results {
    max-height: 400px;
    overflow-x: hidden;
    overflow-y: auto;
  }

  .cover-finder .cover-results.busy * {
    cursor: wait !important;
    pointer-events: none;
  }

  .cover-container {
    padding-top: 133.33%;
    position: relative;
  }

  .cover-container.result {
    cursor: pointer;
  }

  .spinner-border {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    margin: auto;
  }

  .cover-container img {
    display: block;
    position: absolute;
    top: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .config-page {
    padding: 1em;
    border: 1px solid #dee2e6;
    border-top: none;
  }

  td {
    padding: 0 0.5em;
  }

</style>
